package com.swak.frame.datasource.spring.configuration;

import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.ClassPathMapperScanner;
import org.slf4j.Logger;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.StringUtils;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.swak.frame.SwakConstants;
import com.swak.frame.datasource.config.DataBasePropertie;
import com.swak.frame.datasource.config.DataSourcePropertie;
import com.swak.frame.exception.SwakAssert;
import com.swak.frame.logger.SwakLoggerFactory;
import com.swak.frame.mybatis.ReadWriteSqlSessionTemplate;
import com.swak.frame.mybatis.constants.SwakDataBaseConstants;
import com.swak.frame.mybatis.ydd.DynamicDataSource;
import com.swak.frame.mybatis.ydd.DynamicRoutingDataSource;
import com.swak.frame.mybatis.ydd.util.config.YddAppConfig;
import com.swak.frame.registry.SwakBeanDefinitionRegistry;
import com.swak.frame.system.SystemConfigurable;

public class SwakDataSourceRegister extends SwakBeanDefinitionRegistry implements ImportBeanDefinitionRegistrar {

	private Logger logger = SwakLoggerFactory.getLogger(this.getClass());

	private DataSourcePropertie dataSourcePropertie;

	private YddAppConfig yddAppConfig;

	private SystemConfigurable systemConfig;

	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		this.systemConfig = beanFactory.getBean(SystemConfigurable.class);
		this.yddAppConfig = new YddAppConfig(systemConfig.getBizName());
		this.dataSourcePropertie = Binder.get(environment)
				.bind(SwakConstants.SWAK_DATA_SOURCE, DataSourcePropertie.class).get();
		SwakAssert.notNull(dataSourcePropertie,
				"Failed to configure Swak DataSource: '" + SwakConstants.SWAK_DATA_SOURCE
						+ "' attribute is not specified and no embedded datasource could be configured.");

		Map<String, DataBasePropertie> databases = dataSourcePropertie.getDatabase();
		// 配置的database不能为空
		SwakAssert.notEmpty(databases, "Failed to configure Swak DataSource: '" + SwakConstants.SWAK_DATA_SOURCE
				+ "' attribute is not specified and no embedded datasource could be configured.");
		// 循环获取多数据源的配置信息
		for (Entry<String, DataBasePropertie> dataBaseEntry : databases.entrySet()) {
			DataBasePropertie dataBasePropertie = dataBaseEntry.getValue();
			dataBasePropertie.setDbName(dataBaseEntry.getKey());
			// 注册数据库
			registryDataSource(dataBasePropertie, registry);
			// 注册MapperScanner
			registryMapperScanner(dataBasePropertie, registry);
		}
	}

	/**
	 * registry MapperScanner
	 * 
	 * @param dataBasePropertie
	 * @param registry
	 */
	private void registryMapperScanner(DataBasePropertie dataBasePropertie, BeanDefinitionRegistry registry) {
		// 注册MapperScanner
		ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
		scanner.setSqlSessionTemplateBeanName(
				generateBeanName(dataBasePropertie, SwakDataBaseConstants.READ_WRITE_SQLSESSION_TEMPLATE));
		scanner.registerFilters();
		String mapperProperty = dataBasePropertie.getMapper();
		if (mapperProperty == null) {
			logger.error("Failed to configure swak dataSource: swak.datasource.database" + dataBasePropertie.getDbName()
					+ ".mapper cannot be null.");
			return;
		}
		scanner.scan(StringUtils.tokenizeToStringArray(mapperProperty,
				ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
		logger.info("Registration dataSource ({}DataSource) !", dataBasePropertie.getDbName());
	}

	private void registryDataSource(final DataBasePropertie dataBasePropertie, BeanDefinitionRegistry registry) {
		Map<String, SqlSessionFactory> targetSqlSessionFactorys = getSqlSessionFactory(dataBasePropertie);
		SqlSessionFactory writeSqlSessionFactory = targetSqlSessionFactorys.get("write");

		BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
				.genericBeanDefinition(ReadWriteSqlSessionTemplate.class);
		beanDefinitionBuilder.addConstructorArgValue(writeSqlSessionFactory);
		beanDefinitionBuilder.addPropertyValue("targetSqlSessionFactorys", targetSqlSessionFactorys);
		registry.registerBeanDefinition(
				generateBeanName(dataBasePropertie, SwakDataBaseConstants.READ_WRITE_SQLSESSION_TEMPLATE),
				beanDefinitionBuilder.getBeanDefinition());
	}

	// 注册mysqlJdbcTemplate
	private Map<String, SqlSessionFactory> getSqlSessionFactory(final DataBasePropertie dataBasePropertie) {
		Map<String, DynamicRoutingDataSource> allRoutingDataSource = getDynamicRoutingDataSource(dataBasePropertie);
		// 注册writeSqlSessionFactory
		Map<String, Object> writeSqlSessionFactory = Maps.newHashMap();
		writeSqlSessionFactory.put("dataSource", allRoutingDataSource.get("write"));
		writeSqlSessionFactory.put("configLocation", dataSourcePropertie.getConfigLocation());
		writeSqlSessionFactory.put("mapperLocations", dataBasePropertie.getMapperLocations());
		registerIfNotExists(generateBeanName(dataBasePropertie, SwakDataBaseConstants.WRITE_SQLSESSIONFACTORY),
				SqlSessionFactoryBean.class, writeSqlSessionFactory);

		// 注册readSqlSessionFactory
		Map<String, Object> readSqlSessionFactory = Maps.newHashMap();
		readSqlSessionFactory.put("dataSource", allRoutingDataSource.get("read"));
		readSqlSessionFactory.put("configLocation", dataSourcePropertie.getConfigLocation());
		readSqlSessionFactory.put("mapperLocations", dataBasePropertie.getMapperLocations());
		registerIfNotExists(generateBeanName(dataBasePropertie, SwakDataBaseConstants.READ_SQLSESSIONFACTORY),
				SqlSessionFactoryBean.class, readSqlSessionFactory);

		Map<String, SqlSessionFactory> sqlSessionFactorys = ImmutableMap.of("write",
				beanFactory.getBean(generateBeanName(dataBasePropertie, SwakDataBaseConstants.WRITE_SQLSESSIONFACTORY),
						SqlSessionFactory.class),
				"read",
				beanFactory.getBean(generateBeanName(dataBasePropertie, SwakDataBaseConstants.READ_SQLSESSIONFACTORY),
						SqlSessionFactory.class));

		return sqlSessionFactorys;
	}

	private Map<String, DynamicRoutingDataSource> getDynamicRoutingDataSource(
			final DataBasePropertie dataBasePropertie) {
		DynamicDataSource masterDataSource = buildDynamicDatabase(dataBasePropertie.getMaster(),
				generateBeanName(dataBasePropertie, SwakDataBaseConstants.MASTER), yddAppConfig);
		// 注入备库
		String[] slaves = Optional.ofNullable(dataBasePropertie.getSlaves()).orElse(ArrayUtils.EMPTY_STRING_ARRAY);
		Map<String, DynamicDataSource> slavesDataSource = Maps.newHashMap();

		for (int i = 0; i < slaves.length; i++) {
			String slavesConfFile = slaves[i];
			if (!StringUtils.hasText(slavesConfFile)) {
				continue;
			}
			String slavesBeanName = generateBeanName(dataBasePropertie, "mysql_dataSoruce_slave" + i);
			DynamicDataSource slaveDataSource = buildDynamicDatabase(slavesConfFile, slavesBeanName, yddAppConfig);
			slavesDataSource.put(slavesBeanName, slaveDataSource);
		}
		// slaves is empty master replace
		if (MapUtils.isEmpty(slavesDataSource)) {
			slavesDataSource.put(generateBeanName(dataBasePropertie, "mysql_dataSoruce_slave0"),
					masterDataSource);
		}

		// DynamicRoutingDataSource
		DynamicRoutingDataSource dataSourceRead = buildDynamicRoutingDatabase(
				generateBeanName(dataBasePropertie, SwakDataBaseConstants.ROUTING_READ), slavesDataSource,
				masterDataSource, false);
		DynamicRoutingDataSource dataSourceWrite = buildDynamicRoutingDatabase(
				generateBeanName(dataBasePropertie, SwakDataBaseConstants.ROUTING_WRITE),
				ImmutableMap.of(generateBeanName(dataBasePropertie, SwakDataBaseConstants.MASTER),
						masterDataSource),
				slavesDataSource.values().iterator().next(), dataBasePropertie.isPrimary());

		// 注入JdbcTemplate
		registerIfNotExists(generateBeanName(dataBasePropertie, "mysqlJdbcTemplate"), JdbcTemplate.class,
				ImmutableMap.of("dataSource", masterDataSource), dataBasePropertie.isPrimary());

		return ImmutableMap.of("write", dataSourceWrite, "read", dataSourceRead);
	}

	private DynamicRoutingDataSource buildDynamicRoutingDatabase(String beanName,
			Map<String, DynamicDataSource> targetDataSources, DynamicDataSource defaultTargetDataSource, boolean isPrimary) {
		if (!beanFactory.containsBeanDefinition(beanName)) {
			BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
					.genericBeanDefinition(DynamicRoutingDataSource.class);
			beanDefinitionBuilder.addPropertyValue("targetDataSources", targetDataSources);
			beanDefinitionBuilder.addPropertyValue("defaultTargetDataSource", defaultTargetDataSource);
			beanDefinitionBuilder.setPrimary(isPrimary);// see @Primary
			BeanDefinition lastBeanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
			beanFactory.registerBeanDefinition(beanName, lastBeanDefinition);
		}
		return beanFactory.getBean(beanName, DynamicRoutingDataSource.class);
	}

	private DynamicDataSource buildDynamicDatabase(String configFile, String beanName, YddAppConfig yddAppConfig) {
		if (!beanFactory.containsBeanDefinition(beanName)) {
			BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
					.genericBeanDefinition(DynamicDataSource.class);
			beanDefinitionBuilder.addPropertyValue("yddAppConfig", yddAppConfig);
			beanDefinitionBuilder.addPropertyValue("poolId", beanName);
			beanDefinitionBuilder.addPropertyValue("dataId", beanName);
			beanDefinitionBuilder.addPropertyValue("configFileLocation", configFile);
			beanDefinitionBuilder.setLazyInit(true);
			beanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());
		}
		return beanFactory.getBean(beanName, DynamicDataSource.class);
	}

	private String generateBeanName(DataBasePropertie dataBasePropertie, String name) {
		if (dataBasePropertie.isPrimary()) {
			return name;
		}
		return dataBasePropertie.getDbName() + name;
	}
}
